Dependency injection is a technique used in object-oriented programming (OOP) to reduce the hardcoded dependencies between objects. A dependency in this context refers to a piece of code that relies on another resource to carry out its intended function. Often, that resource is a different object in the same application.
Dependencies within an OOP application enable objects to perform their assigned tasks by providing additional functionality. For example, an application might include two class definitions: Class A and Class B. As part of its definition, Class B creates an instance of Class A to carry out a specific task, which means that Class B is dependent on Class A to carry out its function. The dependency is hardcoded into the Class B definition, resulting in code that is tightly coupled. Such code is more difficult to test, modify or reuse than loosely coupled code.
Instead of the dependency being hardcoded, it can be injected through a mechanism such as a class constructor or public property. In this scenario, Class A gets passed into Class B via a parameter, rather than Class B creating the object itself. Class B can then be compiled without including the entire Class A definition, resulting in a class that functions independently of its dependencies. The result is code that is more readable, maintainable, testable, reusable and flexible than tightly coupled code.
Dependency inversion is of particular importance when it comes to dependency injection. Dependency inversion focuses on decoupling and abstracting code, rather than relying too heavily on concretions, which are hardcoded concrete implementations. Dependency inversion also ensures that high-level modules do not depend on low-level modules.
Dependency injection supports the dependency inversion principle by injecting dependencies into the class definitions instead of hardcoding them. In this way, it abstracts the details and ensures that high-level modules don't depend on low-level modules.
Many development teams use dependency injection because it offers several important benefits:
Although dependency injection can be beneficial, it also comes with several challenges:
OOP supports the following approaches to dependency injection:
SaveChanges()
method. EF also provides the asynchronous SaveChangesAsync()
method.Let’s say we have an interface IService
, and three different classes (ServiceA
, ServiceB
, ServiceC
) that share common functionality.
Each class provides its own implementation of Execute()
. No shared code exists among them.
If there’s common functionality across ServiceA
, ServiceB
, and ServiceC
, we can introduce an abstract class:
public interface IService
{
void Execute();
}
public abstract class BaseService : IService
{
public void Log() => Console.WriteLine("Logging action"); // Common functionality
public abstract void Execute(); // Forces derived classes to implement this
}
public class ServiceA : BaseService
{
public override void Execute()
{
Log();
Console.WriteLine("Executing Service A");
}
}
public class ServiceB : BaseService
{
public override void Execute()
{
Log();
Console.WriteLine("Executing Service B");
}
}
public class ServiceC : BaseService
{
public override void Execute()
{
Log();
Console.WriteLine("Executing Service C");
}
}
Here, BaseService
provides shared functionality (e.g., Log()
) so that ServiceA
, ServiceB
, and ServiceC
don’t have to repeat the same logic.
Feature | Interface Only | Abstract Base Class |
---|---|---|
Forces contract adherence | ✅ Yes, all classes must implement the methods | ✅ Yes, but can provide default behavior |
Allows multiple inheritance | ✅ Yes (C# supports multiple interfaces) | ❌ No (C# doesn’t support multiple inheritance for classes) |
Allows shared implementation | ❌ No, each class must provide its own implementation | ✅ Yes, common functionality can be placed in the base class |
Flexibility | ✅ More flexible; any class can implement it without worrying about a base class | ❌ Less flexible; forces all classes to derive from the base class |
Scalability | ✅ Easy to scale and extend | ✅ Good if many classes share behavior |
Testability | ✅ Can be mocked easily | ✅ Can be mocked, but harder if logic is mixed |
Encapsulation | ❌ No default behavior | ✅ Can provide reusable, hidden logic |
Dependency Injection (DI) | ✅ Works well with DI | ✅ Works well, but makes DI a little more complex if constructor logic exists |
✔️ You need maximum flexibility – Any class can implement IService
without being tied to a base class.
✔️ Each class has very different implementations – If ServiceA
, ServiceB
, and ServiceC
have nothing in common besides the method signature.
✔️ You might need multiple inheritance – Since C# doesn’t support multiple class inheritance, an interface allows a class to implement multiple behaviors.
✔️ You’re focusing on dependency inversion – Interfaces work well with DI and keep components loosely coupled.
✔️ There’s shared behavior across implementations – If all service classes require a Log()
method or any other common logic.
✔️ You want partial implementation – The base class can implement default behavior while forcing subclasses to implement required methods.
✔️ The hierarchy makes sense logically – If all services are conceptually similar and should extend a base class.
✔️ You want to reduce code duplication – The base class can contain reusable logic.
Sometimes, you might use both an interface and an abstract class for more flexibility:
csharp
CopyEdit
public interface IService
{
void Execute();
}
public abstract class BaseService : IService
{
public void Log() => Console.WriteLine("Logging action");
public abstract void Execute();
}
public class ServiceA : BaseService
{
public override void Execute()
{
Log();
Console.WriteLine("Executing Service A");
}
}
This way:
BaseService
provides common behavior without enforcing its use.Would you like a real-world ASP.NET Core example, like how DI containers work with abstract classes vs. interfaces? 🚀